﻿/******************************************************************************************************
Copyright (c) 2014 Steven Giacomelli (stevegiacomelli@gmail.com)

Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies
or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************************************************************************************/

using System.Collections.Generic;
using System.IO;
using System.Text;

namespace SharpGeo.IO.WellKnown
{
    /// <summary>
    /// Parses the WellKnowntext format into a WellKnownGraph
    /// </summary>
    public static class WellKnownText
    {
        #region Methods

        #region Helper Methods

        private static bool IsGeometryKeyword(WellKnownKeyword keyword)
        {
            switch (keyword)
            {
                //*** 2D geometries
                case WellKnownKeyword.Geometry:
                case WellKnownKeyword.Point:
                case WellKnownKeyword.LineString:
                case WellKnownKeyword.Polygon:
                case WellKnownKeyword.Multipoint:
                case WellKnownKeyword.MultiLineString:
                case WellKnownKeyword.MultiPolygon:
                case WellKnownKeyword.GeometryCollection:
                case WellKnownKeyword.CircularString:
                case WellKnownKeyword.CompoundCurve:
                case WellKnownKeyword.CurvePolygon:
                case WellKnownKeyword.MultiCurve:
                case WellKnownKeyword.MultiSurface:
                case WellKnownKeyword.Curve:
                case WellKnownKeyword.Surface:
                case WellKnownKeyword.PolyhedralSurface:
                case WellKnownKeyword.TriangulatedIrregularNetwork:
                case WellKnownKeyword.Triangle:

                //*** 3D geometries
                case WellKnownKeyword.GeometryZ:
                case WellKnownKeyword.PointZ:
                case WellKnownKeyword.LineStringZ:
                case WellKnownKeyword.PolygonZ:
                case WellKnownKeyword.MultiPointZ:
                case WellKnownKeyword.MultiLineStringZ:
                case WellKnownKeyword.MultiPolygonZ:
                case WellKnownKeyword.GeometryCollectionZ:
                case WellKnownKeyword.CircularStringZ:
                case WellKnownKeyword.CompoundCurveZ:
                case WellKnownKeyword.CurvePolygonZ:
                case WellKnownKeyword.MultiCurveZ:
                case WellKnownKeyword.MultiSurfaceZ:
                case WellKnownKeyword.CurveZ:
                case WellKnownKeyword.SurfaceZ:
                case WellKnownKeyword.PolyhedralSurfaceZ:
                case WellKnownKeyword.TriangulatedIrregularNetworkZ:
                case WellKnownKeyword.TriangleZ:

                //*** 2D geometries with M
                case WellKnownKeyword.GeometryM:
                case WellKnownKeyword.PointM:
                case WellKnownKeyword.LineStringM:
                case WellKnownKeyword.PolygonM:
                case WellKnownKeyword.MultiPointM:
                case WellKnownKeyword.MultiLineStringM:
                case WellKnownKeyword.MultiPolygonM:
                case WellKnownKeyword.GeometryCollectionM:
                case WellKnownKeyword.CircularStringM:
                case WellKnownKeyword.CompoundCurveM:
                case WellKnownKeyword.CurvePolygonM:
                case WellKnownKeyword.MultiCurveM:
                case WellKnownKeyword.MultiSurfaceM:
                case WellKnownKeyword.CurveM:
                case WellKnownKeyword.SurfaceM:
                case WellKnownKeyword.PolyhedralSurfaceM:
                case WellKnownKeyword.TriangulatedIrregularNetworkM:
                case WellKnownKeyword.TriangleM:

                //*** 3D geometries with M
                case WellKnownKeyword.GeometryZM:
                case WellKnownKeyword.PointZM:
                case WellKnownKeyword.LineStringZM:
                case WellKnownKeyword.PolygonZM:
                case WellKnownKeyword.MultiPointZM:
                case WellKnownKeyword.MultiLineStringZM:
                case WellKnownKeyword.MultiPolygonZM:
                case WellKnownKeyword.GeometryCollectionZM:
                case WellKnownKeyword.CircularStringZM:
                case WellKnownKeyword.CompoundCurveZM:
                case WellKnownKeyword.CurvePolygonZM:
                case WellKnownKeyword.MultiCurveZM:
                case WellKnownKeyword.MultiSurfaceZM:
                case WellKnownKeyword.CurveZM:
                case WellKnownKeyword.SurfaceZM:
                case WellKnownKeyword.PolyhedralSurfaceZM:
                case WellKnownKeyword.TriangulatedIrregularNetworkZM:
                case WellKnownKeyword.TriangleZM:
                    return true;

                //*** All other tokens
                case WellKnownKeyword.Unknown:
                case WellKnownKeyword.ProjectedCoordinateReferenceSystem:
                case WellKnownKeyword.GeographicCoordinateReferenceSystem:
                case WellKnownKeyword.GeocentricCoordinateReferenceSystem:
                case WellKnownKeyword.Datum:
                case WellKnownKeyword.Projection:
                case WellKnownKeyword.Parameter:
                case WellKnownKeyword.Spheroid:
                case WellKnownKeyword.PrimeMeridian:
                case WellKnownKeyword.Unit:
                case WellKnownKeyword.Authority:
                case WellKnownKeyword.ParameterizedMathTransform:
                case WellKnownKeyword.PassThroughMathTransform:
                case WellKnownKeyword.ConcatenatedMathTransform:
                case WellKnownKeyword.InverseMathTransform:
                case WellKnownKeyword.VerticalCoordinateSystem:
                case WellKnownKeyword.VerticalDatum:
                case WellKnownKeyword.CompoundCoordinateSystem:
                case WellKnownKeyword.Axis:
                case WellKnownKeyword.ToWgs84Transform:
                case WellKnownKeyword.FittedCoordinateSystem:
                case WellKnownKeyword.LocalCoordinateSystem:
                case WellKnownKeyword.LocalDatum:
                case WellKnownKeyword.GraphRoot:
                case WellKnownKeyword.Empty:
                case WellKnownKeyword.Value:
                case WellKnownKeyword.ValueSet:
                default:
                    return false;
            }
        }

        #endregion Helper Methods

        #region Parsing Methods

        /// <summary>
        /// Attempts to parse a WellKnownText from a stream
        /// </summary>
        /// <param name="stream">The stream to read the WellKnownText from</param>
        /// <returns>A parsed WellKnownGraph or null if the WellKnownText is badly formed</returns>
        public static WellKnownGraph Parse(Stream stream)
        {
            if (stream == null || !stream.CanRead)
            {
                return null;
            }

            string wkt = string.Empty;
            using (var reader = new StreamReader(stream, Encoding.ASCII))
            {
                wkt = reader.ReadToEnd();
            }

            return Parse(wkt);
        }

        /// <summary>
        /// Attempts to parse a WellKnownText string
        /// </summary>
        /// <param name="wellKnownText">The WellKnownText string</param>
        /// <returns>A parsed WellKnownGraph or null if the WellKnownText is badly formed</returns>
        public static WellKnownGraph Parse(string wellKnownText)
        {
            if (string.IsNullOrWhiteSpace(wellKnownText))
                return null;

            //**** Design Logic********
            //* This parser attempts to parse a block of WellKnownText
            //* Only the graph is built, there is no attempt to validate the relationships between
            //* the keywords and the children/values; this is left out to not constrain the design of other objects
            //* in this class library or extensions
            //*************************

#if !DEBUG
            try
            {
#endif
            //declare a root in case this WKT consists of multiple top level entries
            var root = new WellKnownGraph()
            {
                Keyword = WellKnownKeyword.GraphRoot
            };

            //the token (keyword/value) to build from enumerating the WKT string
            string token = string.Empty;

            //the stack used to store parent/child nodes
            var stack = new Stack<WellKnownGraph>();

            //the current graph used to store information
            var currentGraph = root;

            //track if we are in quoted text so that we ignore delimiting tokens
            bool quotesOpened = false;

            //enumerate through the wkt string
            for (int i = 0; i < wellKnownText.Length; i++)
            {
                //check for the right delimiter
                //if we are not quoted then the token preceding this character is a keyword token
                if (!quotesOpened && (wellKnownText[i] == '[' || wellKnownText[i] == '('))
                {
                    var keyword = WellKnownKeyword.Unknown;

                    //if the token is not whitespace, determine the keyword it represents
                    if (!string.IsNullOrWhiteSpace(token))
                    {
                        keyword = ParseWellKnownToken(token, currentGraph);
                    }
                    //if the token is whitespace than this is a value set
                    else
                    {
                        keyword = WellKnownKeyword.ValueSet;
                    }

                    //if the keyword returned indicates an empty set, ignore
                    if (keyword != WellKnownKeyword.Empty)
                    {
                        //the currentGraph is now the parent of the new graph
                        stack.Push(currentGraph);

                        //create a new graph and assign it the tokenized keyword
                        currentGraph = new WellKnownGraph()
                        {
                            Keyword = keyword
                        };
                    }

                    token = string.Empty;
                }
                else if (!quotesOpened && //if the quotes are not opened and
                    (wellKnownText[i] == ']' || wellKnownText[i] == ')')) //this is a right delimiter
                {
                    //if the current token is not empty than we will grab it as it is the last value
                    if (!string.IsNullOrWhiteSpace(token))
                    {
                        currentGraph.AddValue(token.Trim());
                        token = string.Empty;
                    }

                    //check to see if there is actually a parent on the stack,
                    //malformed WKT with an extra right delimiter will cause this error
                    //so we will just ignore it
                    if (stack.Count > 0)
                    {
                        //Assign the current graph as a child of the graph on the stack
                        var tempGraph = currentGraph;
                        currentGraph = stack.Pop();
                        currentGraph.AddChild(tempGraph);
                    }
                }
                //if the current character is a comma,
                //than the token is a value, store the value and wait for the next value
                else if (!quotesOpened && wellKnownText[i] == ',')
                {
                    if (!string.IsNullOrWhiteSpace(token))
                    {
                        currentGraph.AddValue(token.Trim());
                        token = string.Empty;
                    }
                }
                else if (wellKnownText[i] == '"')
                //if we encounter quotes than toggle the quotes opened var
                //NOTE this does not allow for escaped quotes
                {
                    quotesOpened = !quotesOpened;
                    token += wellKnownText[i]; //save the quotes
                }
                else
                {
                    if (char.IsWhiteSpace(wellKnownText[i]))
                    {
                        //check to see if the previous char was whitespace, if yes ignore this char
                        if (token.Length > 0)
                        {
                            if (token[token.Length - 1] == ' ')
                            {
                                continue;
                            }
                        }

                        //we only want this kind of whitespace
                        token += ' ';
                    }
                    else
                    {
                        token += wellKnownText[i];
                    }
                }
            }

            token = token.Trim();

            //if we have tokens values left
            if (!string.IsNullOrWhiteSpace(token))
            {
                var keyword = ParseWellKnownToken(token, currentGraph);

                if (keyword != WellKnownKeyword.Unknown)
                {
                    currentGraph.AddChild(new WellKnownGraph()
                    {
                        Keyword = keyword
                    });
                }
            }

            //if we still have items on the stack it is because the WKT is malformed and we are missing right delimiters
            //silently fix this problem
            while (stack.Count > 0)
            {
                var tempGraph = currentGraph;
                currentGraph = stack.Pop();
                currentGraph.AddChild(tempGraph);
            }

            //if there is only one root return the child as the root
            if (root.Count == 1)
            {
                root = root[0];
            }

            return root;
#if !DEBUG
            }
            catch (Exception)
            {
                return null;
            }
#endif
        }

        private static WellKnownKeyword ParseWellKnownToken(string token, WellKnownGraph currentGraph)
        {
            var keyword = WellKnownKeyword.Unknown;

            //in order to parse space delimited keywords ie POINT ZM, we will split our token
            //with spaces (note that we have ensured that we only have one space ' ' in our
            //tokens)
            var keywordValues = token.Trim().Split(' ');

            //if there is only keyword short circut the other logic
            if (keywordValues.Length == 1)
            {
                keyword = GetWellKnownKeyword(keywordValues[0]);
            }
            else
            {
                //loop through each token
                for (int j = 0; j < keywordValues.Length; j++)
                {
                    keyword = GetWellKnownKeyword(keywordValues[j]);

                    //if this is the last token.. break the loop
                    if (j + 1 >= keywordValues.Length)
                    {
                        break;
                    }

                    var nextKeyword = GetWellKnownKeyword(keywordValues[j + 1]);

                    if (nextKeyword != WellKnownKeyword.Empty)
                    {
                        //if this is a geometry object and the next keyword does not indicate an
                        //empty set - see if there is a ZM,Z, or M value waiting, otherwise
                        //silently add an empty set to the current graph
                        if (IsGeometryKeyword(keyword))
                        {
                            //if the next token is not empty and this is a geometry keyword,
                            //attempt to see if this is a ZM, Z, or M set

                            var combinedGeometryKeyword =
                                CombineGeometryKeyword(keywordValues[j + 1], keyword);

                            //we failed to combine the keyword
                            //silently add an empty set
                            if (combinedGeometryKeyword == WellKnownKeyword.Unknown)
                            {
                                currentGraph.AddChild(new WellKnownGraph()
                                {
                                    Keyword = keyword
                                });

                                continue;
                            }

                            keyword = combinedGeometryKeyword;

                            //if this is the last token.. break the loop
                            if (j + 2 >= keywordValues.Length)
                            {
                                break;
                            }

                            nextKeyword = GetWellKnownKeyword(keywordValues[j + 2]);
                            j++;
                        }
                        //so if the next value is not empty and this is NOT a geometry set than
                        //just silently add the keyword as an empty child set
                        else
                        {
                            currentGraph.AddChild(new WellKnownGraph()
                            {
                                Keyword = keyword
                            });

                            continue;
                        }
                    }

                    //this is not a chained if/else to allow for fallthrough logic
                    //if it is empty, just assume this is an empty set
                    if (nextKeyword == WellKnownKeyword.Empty)
                    {
                        currentGraph.AddChild(new WellKnownGraph()
                        {
                            Keyword = keyword
                        });

                        keyword = WellKnownKeyword.Empty;
                        j++;

                        continue;
                    }
                }
            }

            return keyword;
        }

        private static WellKnownKeyword GetWellKnownKeyword(string token)
        {
            switch (token.Trim().ToUpperInvariant())
            {
                // ***** Geometries
                case "GEOMETRY":
                    return WellKnownKeyword.Geometry;

                case "POINT":
                    return WellKnownKeyword.Point;

                case "LINESTRING":
                    return WellKnownKeyword.LineString;

                case "POLYGON":
                    return WellKnownKeyword.Polygon;

                case "MULTIPOINT":
                    return WellKnownKeyword.Multipoint;

                case "MULTILINESTRING":
                    return WellKnownKeyword.MultiLineString;

                case "MULTIPOLYGON":
                    return WellKnownKeyword.MultiPolygon;

                case "GEOMETRYCOLLECTION":
                    return WellKnownKeyword.GeometryCollection;

                case "CIRCULARSTRING":
                    return WellKnownKeyword.CircularString;

                case "COMPOUNDCURVE":
                    return WellKnownKeyword.CompoundCurve;

                case "CURVEPOLYGON":
                    return WellKnownKeyword.CurvePolygon;

                case "MULTICURVE":
                    return WellKnownKeyword.MultiCurve;

                case "MULTISURFACE":
                    return WellKnownKeyword.MultiSurface;

                case "CURVE":
                    return WellKnownKeyword.Curve;

                case "SURFACE":
                    return WellKnownKeyword.Surface;

                case "POLYHEDRON":
                case "POLYHEDRALSURFACE":
                    return WellKnownKeyword.PolyhedralSurface;

                case "TIN":
                    return WellKnownKeyword.TriangulatedIrregularNetwork;

                case "TRIANGLE":
                    return WellKnownKeyword.Triangle;

                //******* empty
                case "EMPTY":
                    return WellKnownKeyword.Empty;

                //****** Coordinate Reference Systems
                case "PROJCS":
                    return WellKnownKeyword.ProjectedCoordinateReferenceSystem;

                case "GEOGCS":
                    return WellKnownKeyword.GeographicCoordinateReferenceSystem;

                case "GEOCCS":
                    return WellKnownKeyword.GeographicCoordinateReferenceSystem;

                case "VERT_CS":
                    return WellKnownKeyword.VerticalCoordinateSystem;

                case "VERT_DATUM":
                    return WellKnownKeyword.VerticalDatum;

                case "COMPD_CS":
                    return WellKnownKeyword.CompoundCoordinateSystem;

                case "AXIS":
                    return WellKnownKeyword.Axis;

                case "TOWGS84":
                    return WellKnownKeyword.ToWgs84Transform;

                case "FITTED_CS":
                    return WellKnownKeyword.FittedCoordinateSystem;

                case "LOCAL_CS":
                    return WellKnownKeyword.LocalCoordinateSystem;

                case "LOCAL_DATUM":
                    return WellKnownKeyword.LocalDatum;

                case "DATUM":
                    return WellKnownKeyword.Datum;

                case "PROJECTION":
                    return WellKnownKeyword.Projection;

                case "PARAMETER":
                    return WellKnownKeyword.Parameter;

                case "SPHEROID":
                    return WellKnownKeyword.Spheroid;

                case "PRIMEM":
                    return WellKnownKeyword.PrimeMeridian;

                case "UNIT":
                    return WellKnownKeyword.Unit;

                case "AUTHORITY":
                    return WellKnownKeyword.Authority;

                //******Math Transforms
                case "PARAM_MT":
                    return WellKnownKeyword.ParameterizedMathTransform;

                case "CONCAT_MT":
                    return WellKnownKeyword.ConcatenatedMathTransform;

                case "INVERSE_MT":
                    return WellKnownKeyword.InverseMathTransform;

                case "PASSTHROUGH_MT":
                    return WellKnownKeyword.PassThroughMathTransform;

                default:
                    return WellKnownKeyword.Unknown;
            }
        }

        private static WellKnownKeyword CombineGeometryKeyword(string value, WellKnownKeyword keyword)
        {
            var t = value.Trim().ToUpperInvariant();

            if (t == "ZM")
            {
                switch (keyword)
                {
                    case WellKnownKeyword.Geometry:
                        return WellKnownKeyword.GeometryZM;

                    case WellKnownKeyword.Point:
                        return WellKnownKeyword.PointZM;

                    case WellKnownKeyword.LineString:
                        return WellKnownKeyword.LineStringZM;

                    case WellKnownKeyword.Polygon:
                        return WellKnownKeyword.PolygonZM;

                    case WellKnownKeyword.Multipoint:
                        return WellKnownKeyword.MultiPointZM;

                    case WellKnownKeyword.MultiLineString:
                        return WellKnownKeyword.MultiLineStringZM;

                    case WellKnownKeyword.MultiPolygon:
                        return WellKnownKeyword.MultiPolygonZM;

                    case WellKnownKeyword.GeometryCollection:
                        return WellKnownKeyword.GeometryCollectionZM;

                    case WellKnownKeyword.CircularString:
                        return WellKnownKeyword.CircularStringZM;

                    case WellKnownKeyword.CompoundCurve:
                        return WellKnownKeyword.CompoundCurveZM;

                    case WellKnownKeyword.CurvePolygon:
                        return WellKnownKeyword.CurvePolygonZM;

                    case WellKnownKeyword.MultiCurve:
                        return WellKnownKeyword.MultiCurveZM;

                    case WellKnownKeyword.MultiSurface:
                        return WellKnownKeyword.MultiSurfaceZM;

                    case WellKnownKeyword.Curve:
                        return WellKnownKeyword.CurveZM;

                    case WellKnownKeyword.Surface:
                        return WellKnownKeyword.SurfaceZM;

                    case WellKnownKeyword.PolyhedralSurface:
                        return WellKnownKeyword.PolyhedralSurfaceZM;

                    case WellKnownKeyword.TriangulatedIrregularNetwork:
                        return WellKnownKeyword.TriangulatedIrregularNetworkZM;

                    case WellKnownKeyword.Triangle:
                        return WellKnownKeyword.TriangleZM;

                    default:
                        return WellKnownKeyword.Unknown;
                }
            }
            else if (t == "M")
            {
                switch (keyword)
                {
                    case WellKnownKeyword.Geometry:
                        return WellKnownKeyword.GeometryM;

                    case WellKnownKeyword.Point:
                        return WellKnownKeyword.PointM;

                    case WellKnownKeyword.LineString:
                        return WellKnownKeyword.LineStringM;

                    case WellKnownKeyword.Polygon:
                        return WellKnownKeyword.PolygonM;

                    case WellKnownKeyword.Multipoint:
                        return WellKnownKeyword.MultiPointM;

                    case WellKnownKeyword.MultiLineString:
                        return WellKnownKeyword.MultiLineStringM;

                    case WellKnownKeyword.MultiPolygon:
                        return WellKnownKeyword.MultiPolygonM;

                    case WellKnownKeyword.GeometryCollection:
                        return WellKnownKeyword.GeometryCollectionM;

                    case WellKnownKeyword.CircularString:
                        return WellKnownKeyword.CircularStringM;

                    case WellKnownKeyword.CompoundCurve:
                        return WellKnownKeyword.CompoundCurveM;

                    case WellKnownKeyword.CurvePolygon:
                        return WellKnownKeyword.CurvePolygonM;

                    case WellKnownKeyword.MultiCurve:
                        return WellKnownKeyword.MultiCurveM;

                    case WellKnownKeyword.MultiSurface:
                        return WellKnownKeyword.MultiSurfaceM;

                    case WellKnownKeyword.Curve:
                        return WellKnownKeyword.CurveM;

                    case WellKnownKeyword.Surface:
                        return WellKnownKeyword.SurfaceM;

                    case WellKnownKeyword.PolyhedralSurface:
                        return WellKnownKeyword.PolyhedralSurfaceM;

                    case WellKnownKeyword.TriangulatedIrregularNetwork:
                        return WellKnownKeyword.TriangulatedIrregularNetworkM;

                    case WellKnownKeyword.Triangle:
                        return WellKnownKeyword.TriangleM;

                    default:
                        return WellKnownKeyword.Unknown;
                }
            }
            else if (t == "Z")
            {
                switch (keyword)
                {
                    case WellKnownKeyword.Geometry:
                        return WellKnownKeyword.GeometryZ;

                    case WellKnownKeyword.Point:
                        return WellKnownKeyword.PointZ;

                    case WellKnownKeyword.LineString:
                        return WellKnownKeyword.LineStringZ;

                    case WellKnownKeyword.Polygon:
                        return WellKnownKeyword.PolygonZ;

                    case WellKnownKeyword.Multipoint:
                        return WellKnownKeyword.MultiPointZ;

                    case WellKnownKeyword.MultiLineString:
                        return WellKnownKeyword.MultiLineStringZ;

                    case WellKnownKeyword.MultiPolygon:
                        return WellKnownKeyword.MultiPolygonZ;

                    case WellKnownKeyword.GeometryCollection:
                        return WellKnownKeyword.GeometryCollectionZ;

                    case WellKnownKeyword.CircularString:
                        return WellKnownKeyword.CircularStringZ;

                    case WellKnownKeyword.CompoundCurve:
                        return WellKnownKeyword.CompoundCurveZ;

                    case WellKnownKeyword.CurvePolygon:
                        return WellKnownKeyword.CurvePolygonZ;

                    case WellKnownKeyword.MultiCurve:
                        return WellKnownKeyword.MultiCurveZ;

                    case WellKnownKeyword.MultiSurface:
                        return WellKnownKeyword.MultiSurfaceZ;

                    case WellKnownKeyword.Curve:
                        return WellKnownKeyword.CurveZ;

                    case WellKnownKeyword.Surface:
                        return WellKnownKeyword.SurfaceZ;

                    case WellKnownKeyword.PolyhedralSurface:
                        return WellKnownKeyword.PolyhedralSurfaceZ;

                    case WellKnownKeyword.TriangulatedIrregularNetwork:
                        return WellKnownKeyword.TriangulatedIrregularNetworkZ;

                    case WellKnownKeyword.Triangle:
                        return WellKnownKeyword.TriangleZ;

                    default:
                        return WellKnownKeyword.Unknown;
                }
            }

            return WellKnownKeyword.Unknown;
        }

        #endregion Parsing Methods

        #region Write Methods

        /// <summary>
        /// Writes a WellKnownGraph to WellKnownText using a Stream
        /// </summary>
        /// <param name="graph">The WellKnownGraph to write to WellKnownText</param>
        /// <param name="stream">The stream to write the WellKnownGraph to</param>
        public static void Write(WellKnownGraph graph, Stream stream)
        {
            if (graph == null || stream == null || !stream.CanWrite)
            {
                return;
            }

            using (var writer = new StreamWriter(stream, Encoding.ASCII))
            {
                Write(graph, writer);
            }
        }

        /// <summary>
        /// Writes a WellKnownGraph to WellKnownText using a TextWriter
        /// </summary>
        /// <param name="graph">The WellKnownGraph to write to WellKnownText</param>
        /// <param name="writer">The TextWriter to write the WellKnownText to</param>
        public static void Write(WellKnownGraph graph, TextWriter writer)
        {
            if (graph == null || writer == null)
            {
                return;
            }

            writer.Write(Write(graph));
        }

        /// <summary>
        /// Writes a WellKnownGraph to WellKnownText (string)
        /// </summary>
        /// <param name="graph">The WellKnownGraph to write to WellKnownText</param>
        /// <returns>The WellKnownText representation of the passed WellKnownGraph</returns>
        public static string Write(WellKnownGraph graph)
        {
            //if we are passed a null graph, fail gracefully
            if (graph == null)
            {
                return string.Empty;
            }

            if (graph.Keyword == WellKnownKeyword.GraphRoot)
            {
                var sb = new StringBuilder();

                for (int i = 0; i < graph.Count; i++)
                {
                    var v = WriteRecursive(graph[i]).Trim();
                    if (!string.IsNullOrWhiteSpace(v))
                    {
                        sb.AppendLine(v);
                    }
                }
                return sb.ToString().Trim();
            }
            else
            {
                return WriteRecursive(graph).Trim();
            }
        }

        private static string WriteRecursive(WellKnownGraph graph)
        {
            var sb = new StringBuilder();

            string keyword = GetWellKnownKeyword(graph.Keyword);

            if (IsGeometryKeyword(graph.Keyword))
            {
                sb.Append(keyword);

                if (graph.Count < 1)
                {
                    sb.Append(" EMPTY ");
                }
                else
                {
                    var vals = new List<string>(graph.Count);

                    for (int i = 0; i < graph.Count; i++)
                    {
                        var v = WriteRecursive(graph[i]).Trim();

                        if (!string.IsNullOrWhiteSpace(v))
                        {
                            vals.Add(v);
                        }
                    }

                    if (vals.Count > 0)
                    {
                        sb.AppendFormat("({0}) ", string.Join(",", vals));
                    }
                }
            }
            else
            {
                var data = graph as WellKnownData;

                if (data != null)
                {
                    return data.Value == null ? string.Empty : data.Value.ToString();
                }

                //If this is not a data value and the number of children is zero, return an empty string
                if (!(graph.Count > 0))
                    return string.Empty;

                //if the keyword is not string.empty write it
                if (!string.IsNullOrWhiteSpace(keyword))
                {
                    sb.Append(keyword);
                }

                var vals = new List<string>(graph.Count);

                for (int i = 0; i < graph.Count; i++)
                {
                    var v = WriteRecursive(graph[i]).Trim();

                    if (!string.IsNullOrWhiteSpace(v))
                    {
                        vals.Add(v);
                    }
                }

                if (vals.Count > 0)
                {
                    if (graph.Keyword == WellKnownKeyword.ValueSet)
                    {
                        sb.AppendFormat("({0}) ", string.Join(",", vals));
                    }
                    else
                    {
                        sb.AppendFormat("[{0}] ", string.Join(",", vals));
                    }
                }
            }

            return sb.ToString();
        }

        private static string GetWellKnownKeyword(WellKnownKeyword keyword)
        {
            switch (keyword)
            {
                case WellKnownKeyword.Geometry:
                    return "GEOMETRY";

                case WellKnownKeyword.Point:
                    return "POINT";

                case WellKnownKeyword.LineString:
                    return "LINESTRING";

                case WellKnownKeyword.Polygon:
                    return "POLYGON";

                case WellKnownKeyword.Multipoint:
                    return "MULTIPOINT";

                case WellKnownKeyword.MultiLineString:
                    return "MULTILINESTRING";

                case WellKnownKeyword.MultiPolygon:
                    return "MULTIPOLYGON";

                case WellKnownKeyword.GeometryCollection:
                    return "GEOMETRYCOLLECTION";

                case WellKnownKeyword.CircularString:
                    return "CIRCULARSTRING";

                case WellKnownKeyword.CompoundCurve:
                    return "COMPOUNDCURVE";

                case WellKnownKeyword.CurvePolygon:
                    return "CURVEPOLYGON";

                case WellKnownKeyword.MultiCurve:
                    return "MULTICURVE";

                case WellKnownKeyword.MultiSurface:
                    return "MULTISURFACE";

                case WellKnownKeyword.Curve:
                    return "CURVE";

                case WellKnownKeyword.Surface:
                    return "SURFACE";

                case WellKnownKeyword.PolyhedralSurface:
                    return "POLYHEDRALSURFACE";

                case WellKnownKeyword.TriangulatedIrregularNetwork:
                    return "TIN";

                case WellKnownKeyword.Triangle:
                    return "TRIANGLE";

                //*** 3D geometries
                case WellKnownKeyword.GeometryZ:
                    return "GEOMETRY Z";

                case WellKnownKeyword.PointZ:
                    return "POINT Z";

                case WellKnownKeyword.LineStringZ:
                    return "LINESTRING Z";

                case WellKnownKeyword.PolygonZ:
                    return "POLYGON Z";

                case WellKnownKeyword.MultiPointZ:
                    return "MULTIPOINT Z";

                case WellKnownKeyword.MultiLineStringZ:
                    return "MULTILINESTRING Z";

                case WellKnownKeyword.MultiPolygonZ:
                    return "MULTIPOLYGON Z";

                case WellKnownKeyword.GeometryCollectionZ:
                    return "GEOMETRYCOLLECTION Z";

                case WellKnownKeyword.CircularStringZ:
                    return "CIRCULARSTRING Z";

                case WellKnownKeyword.CompoundCurveZ:
                    return "COMPOUNDCURVE Z";

                case WellKnownKeyword.CurvePolygonZ:
                    return "CURVEPOLYGON Z";

                case WellKnownKeyword.MultiCurveZ:
                    return "MULTICURVE Z";

                case WellKnownKeyword.MultiSurfaceZ:
                    return "MULTISURFACE Z";

                case WellKnownKeyword.CurveZ:
                    return "CURVE Z";

                case WellKnownKeyword.SurfaceZ:
                    return "SURFACE Z";

                case WellKnownKeyword.PolyhedralSurfaceZ:
                    return "POLYHEDRALSURFACE Z";

                case WellKnownKeyword.TriangulatedIrregularNetworkZ:
                    return "TIN Z";

                case WellKnownKeyword.TriangleZ:
                    return "TRIANGLE Z";

                //***2D Geometries with M
                case WellKnownKeyword.GeometryM:
                    return "GEOMETRY M";

                case WellKnownKeyword.PointM:
                    return "POINT M";

                case WellKnownKeyword.LineStringM:
                    return "LINESTRING M";

                case WellKnownKeyword.PolygonM:
                    return "POLYGON M";

                case WellKnownKeyword.MultiPointM:
                    return "MULTIPOINT M";

                case WellKnownKeyword.MultiLineStringM:
                    return "MULTILINESTRING M";

                case WellKnownKeyword.MultiPolygonM:
                    return "MULTIPOLYGON M";

                case WellKnownKeyword.GeometryCollectionM:
                    return "GEOMETRYCOLLECTION M";

                case WellKnownKeyword.CircularStringM:
                    return "CIRCULARSTRING M";

                case WellKnownKeyword.CompoundCurveM:
                    return "COMPOUNDCURVE M";

                case WellKnownKeyword.CurvePolygonM:
                    return "CURVEPOLYGON M";

                case WellKnownKeyword.MultiCurveM:
                    return "MULTICURVE M";

                case WellKnownKeyword.MultiSurfaceM:
                    return "MULTISURFACE M";

                case WellKnownKeyword.CurveM:
                    return "CURVE M";

                case WellKnownKeyword.SurfaceM:
                    return "SURFACE M";

                case WellKnownKeyword.PolyhedralSurfaceM:
                    return "POLYHEDRALSURFACE M";

                case WellKnownKeyword.TriangulatedIrregularNetworkM:
                    return "TIN M";

                case WellKnownKeyword.TriangleM:
                    return "TRIANGLE M";

                //***3D geometries with M
                case WellKnownKeyword.GeometryZM:
                    return "GEOMETRY ZM";

                case WellKnownKeyword.PointZM:
                    return "POINT ZM";

                case WellKnownKeyword.LineStringZM:
                    return "LINESTRING ZM";

                case WellKnownKeyword.PolygonZM:
                    return "POLYGON ZM";

                case WellKnownKeyword.MultiPointZM:
                    return "MULTIPOINT ZM";

                case WellKnownKeyword.MultiLineStringZM:
                    return "MULTILINESTRING ZM";

                case WellKnownKeyword.MultiPolygonZM:
                    return "MULTIPOLYGON ZM";

                case WellKnownKeyword.GeometryCollectionZM:
                    return "GEOMETRYCOLLECTION ZM";

                case WellKnownKeyword.CircularStringZM:
                    return "CIRCULARSTRING ZM";

                case WellKnownKeyword.CompoundCurveZM:
                    return "COMPOUNDCURVE ZM";

                case WellKnownKeyword.CurvePolygonZM:
                    return "CURVEPOLYGON ZM";

                case WellKnownKeyword.MultiCurveZM:
                    return "MULTICURVE ZM";

                case WellKnownKeyword.MultiSurfaceZM:
                    return "MULTISURFACE ZM";

                case WellKnownKeyword.CurveZM:
                    return "CURVE ZM";

                case WellKnownKeyword.SurfaceZM:
                    return "SURFACE ZM";

                case WellKnownKeyword.PolyhedralSurfaceZM:
                    return "POLYHEDRALSURFACE ZM";

                case WellKnownKeyword.TriangulatedIrregularNetworkZM:
                    return "TIN ZM";

                case WellKnownKeyword.TriangleZM:
                    return "TRIANGLE ZM";

                //***Coordinate Reference Systems
                case WellKnownKeyword.ProjectedCoordinateReferenceSystem:
                    return "PROJCS";

                case WellKnownKeyword.GeographicCoordinateReferenceSystem:
                    return "GEOGCS";

                case WellKnownKeyword.GeocentricCoordinateReferenceSystem:
                    return "GEOCCS";

                case WellKnownKeyword.Datum:
                    return "DATUM";

                case WellKnownKeyword.Projection:
                    return "PROJECTION";

                case WellKnownKeyword.Parameter:
                    return "PARAMETER";

                case WellKnownKeyword.Spheroid:
                    return "SPHEROID";

                case WellKnownKeyword.PrimeMeridian:
                    return "PRIMEM";

                case WellKnownKeyword.Unit:
                    return "UNIT";

                case WellKnownKeyword.Authority:
                    return "AUTHORITY";

                case WellKnownKeyword.VerticalCoordinateSystem:
                    return "VERT_CS";

                case WellKnownKeyword.VerticalDatum:
                    return "VERT_DATUM";

                case WellKnownKeyword.CompoundCoordinateSystem:
                    return "COMPD_CS";

                case WellKnownKeyword.Axis:
                    return "AXIS";

                case WellKnownKeyword.ToWgs84Transform:
                    return "TOWGS84";

                case WellKnownKeyword.FittedCoordinateSystem:
                    return "FITTED_CS";

                case WellKnownKeyword.LocalCoordinateSystem:
                    return "LOCAL_CS";

                case WellKnownKeyword.LocalDatum:
                    return "LOCAL_DATUM";

                //******Math Transforms
                case WellKnownKeyword.ParameterizedMathTransform:
                    return "PARAM_MT";

                case WellKnownKeyword.ConcatenatedMathTransform:
                    return "CONCAT_MT";

                case WellKnownKeyword.InverseMathTransform:
                    return "INVERSE_MT";

                case WellKnownKeyword.PassThroughMathTransform:
                    return "PASSTHROUGH_MT";

                //*** if we are a special keyword return string.empty
                case WellKnownKeyword.Value:
                case WellKnownKeyword.Unknown:
                case WellKnownKeyword.GraphRoot:
                case WellKnownKeyword.Empty:
                case WellKnownKeyword.ValueSet:
                default:
                    return string.Empty;
            }
        }

        #endregion Write Methods

        #endregion Methods
    }
}